<?php

// Namespace

namespace Loop;


// Dependencies

require_once(PATH_LOOP . 'Class/IDatabaseDriver.php');


/**
 * CMySqlIDriver allows to run and use the mysqli driver for PHP
 * This class is the implementation of the interface IDatabaseDriver
 *
 * @package		Loop
 * @category	Driver
 */

class CMySqlIDriver implements IDatabaseDriver
{
	// Properties
	
	private $databaseName = null;
	private $extensionId = null;
	private $handle = null;


	/**
	 *
	 */

	public function __construct($extensionId)
	{
		$this->extensionId = $extensionId;
	}


	/**
	 *
	 */

	public function __get($propertyName)
	{
		return $this->$propertyName;
	}
	

	/*
	 *
	 */

	public function closeTransaction()
	{
		if (pref('[Loop]/database/activeTransaction') == true)
		{		
			// Commit transaction
			
			$query = 'COMMIT;';
			$result = $this->executeQuery($query);		


			// Enable autocommit
			
			$query = 'SET AUTOCOMMIT=1';
			$result = $this->executeQuery($query);		
		}
	}

	
	/**
	 * See IDatabaseDriver::connect()
	 */
	
	public function connect($host = null, $login = null, $password = null, $name = null, $port = null, $socket = null)
	{
		// In case we haven't received host/login…, use preferences
		
		if (empty($host) == true)
		{
			$host = pref('[' . $this->extensionId . ']/database/host');
			$login = pref('[' . $this->extensionId . ']/database/login');
			$password = pref('[' . $this->extensionId . ']/database/password');
			$name = pref('[' . $this->extensionId . ']/database/name');
			$port = pref('[' . $this->extensionId . ']/database/port');
			$socket = pref('[' . $this->extensionId . ']/database/socket');
		}


		// First, connect to the server and store the handle
		
		$this->handle = mysqli_connect
		(
			$host,
			$login,
			$password,
			'',
			$port,
			$socket
		);

		
		// Has the connection succeeded?
		
		if ($this->isConnected() == false)
		{
			e
			(
				EXCEPTION_DATABASE_CONNECT_FAILED,
				array
				(
					'host' => $host,
					'login' => $login,
					'port' => $port,
					'socket' => $socket
				)
			);
		}


		// Select the database
		
		if (empty($name) === false)
		{
			$this->selectDatabase($name);
		}


		// Make sure we are working with UTF8 encoding
		
	    mysqli_set_charset($this->handle, 'utf8');
	}
	
	
	/**
	 * See IDatabaseDriver::disconnect()
	 */
	
	public function disconnect()
	{
		// No need to disconnect if there's no connection
		
		if ($this->isConnected() == false)
		{
			return;
		}

		
		// Close TCP socket and disconnect
		
		$threadId = mysqli_thread_id($this->handle);
		
		mysqli_kill($this->handle, $threadId);
		mysqli_close($this->handle);
	}
	
	
	/**
	 * See IDatabaseDriver::dumpData()
	 */
	
	public function dumpData($pathToFile)
	{
		// Get the list of tables (not views)
		
		$tableNames = $this->listTables();
		
		
		// For each table, create a set of INSERT queries
		
		$queries = array();

		foreach ($tableNames as $tableName)
		{			
			// Get every record in the table
			
			$query = 'SELECT * FROM %1';
			$queryVariables = array($tableName);
			
			$result = $this->executeQuery($query, $queryVariables);
			
			
			// Collect fields in the table
			
			$fields = mysqli_fetch_fields($result);
			$nbFields = count($fields);
			$fieldsNames = array();

			foreach ($fields as $field)
			{
				// Store the field name

				$fieldsNames[] = '`' . $field->name . '`';
			}


			// Build the INSERT query
			
			$subQuery = null;
			$separator = null;
			
			while ($row = $this->fetchRow($result))
			{
				if (empty($subQuery) == true)
				{
					$subQuery = 'INSERT INTO `' . $tableName . '`(' . implode(', ', $fieldsNames) . ") VALUES\n";
				}

				$subQuery .= $separator . '(';
				
				for ($i = 0; $i < $nbFields; $i++)
				{
					if (isset($row[$i]) == true)
					{
						$subQuery .= '\'' . addslashes($row[$i]) . '\'' ;
					}
					else
					{
						$subQuery .= 'NULL';
					}
					
					if ($i < ($nbFields - 1))
					{
						$subQuery .= ', ';
					}
				}
				
				$subQuery .= ')';

				if (mb_strlen($subQuery) >= 10000)
				{
					$queries[] = $subQuery;

					$subQuery = null;
					$separator = null;
				}
				else
				{
					$separator = ",\n";
				}
			}


			// Add the last sub-SQL

			if (empty($subQuery) == false)
			{
				$queries[] = $subQuery . ";\n";
			}
		}


		// Save the backup
		
		$dump = implode(";\n", $queries);
		file_put_contents($pathToFile, $dump);
		
		
		return $dump;
	}
	
	
	/**
	 * See IDatabaseDriver::executeQuery()
	 */
	
	public function executeQuery($query, $queryVariables = null, &$finalQuery = null)
	{
		// Start the profiling event

		$puuid = pstart(PROFILING_TYPE_QUERY);


		// Remove empty spaces from SQL

		$query = trim($query);


		// If the SQL query is empty, ignore it
		
		if (empty($query) == true)
		{
			return;
		}
		
		
		// Do we need to connect?
		
		if ($this->isConnected() == false)
		{
			$this->connect();
		}
		
		
		// Make sure we have a ; at the end (can be duplicated, it's ok)
		
		$query .= ';';
		
		
		// Generate the final SQL query
		
		$finalQuery = $this->generateQuery($query, $queryVariables);


		// We will have to store the query for monitoring
		
		$queries = v('::queries');
		

		// Execute the query

		$result = mysqli_query($this->handle, $finalQuery);
		

		// Stop the profiling event

		pstop($puuid, array($finalQuery, $result));


		// The query has failed

		if ($result == false)
		{
			// Throw an exception

			e
			(
				EXCEPTION_DATABASE_QUERY_FAILED,
				array
				(
					'sql' => $finalQuery,
					'error' => mysqli_error($this->handle),
					'code' => mysqli_sqlstate($this->handle)
				)
			);
		}
		
		
		// Store the last query

		v('::lastQuery', $finalQuery);
		v('::nbQueries', v('::nbQueries') + 1);

		
		return $result;
	}
	
	
	/**
	 * See IDatabaseDriver::executeQueries()
	 */
	
	public function executeQueries($queries, $isSecure = true)
	{
		// Do we need to connect?
		
		if ($this->isConnected() == false)
		{
			$this->connect();
		}
		
		
		// Execute the first query
		
		$results = array();
		
		foreach ($queries as $query)
		{
			try
			{
				// Execute the query

				$result[] = $this->executeQuery($query);
			}
			catch (\Exception $e)
			{
				if ($isSecure == true)
				{
					throw $e;
				}
			}
		}
		
		return $results;
	}
	
	
	/**
	 * See IDatabaseDriver::fetchAssociativeArray()
	 */
	
	public function fetchAssociativeArray(&$result)
	{
		return mysqli_fetch_assoc($result);
	}
	
	
	/**
	 * See IDatabaseDriver::fetchAssociativeArray()
	 */
	
	public function fetchRow(&$result)
	{
		return mysqli_fetch_row($result);
	}
	
	
	/**
	 * Generates a SQL query from a set of variables
	 *
	 * @param	string	$query			The SQL query
	 * @param	array	$queryVariables	An array of variables
	 *
	 * @return	string					The final SQL query
	 */
	
	public function generateQuery($query, $queryVariables)
	{
		// Generate the SQL query by injecting secured variables, if any

		if 
		(
			is_array($queryVariables) === true &&
			count($queryVariables) > 0
		)
		{
			$driver = $this;
			
			// 

			$isArrayAssociative = false;
			$pattern = '/%([0-9]+)/';
			
			//
			
			$arrayKeys = array_keys($queryVariables); 

			// Is the array associative ?

			if (is_numeric($arrayKeys[0]) == false)
			{
				$isArrayAssociative = true;
			}

			
			// Set the pattern			

			if ($isArrayAssociative == true)
			{
				$pattern =  '/%\{([a-z-A-Z]+)\}/';
			}


			// Match the query and the associated variables

			$query = preg_replace_callback
        	(
	            $pattern,
	            function($matches) use ($queryVariables, $driver, $isArrayAssociative)
	            {
					// Make sure the variable can safely be injected into the query

					if (isset($matches[1]) == true)
					{
						$key = $matches[1]; 

						if ($isArrayAssociative == false)
						{
							$key -= 1; 
						}

						if (isset($queryVariables[$key]) == true)
						{
							return $driver->secureString($queryVariables[$key]);
						}
						else
		            	{
	            			// Object not found 

	            			e
	            			(
	            				EXCEPTION_VARIABLE_NOT_FOUND,
	            				array
	            				(
	            					'key' => $key
	            				)
	            			);
		            	}
					}
	            },
	            $query
        	); 
		}

		
		return $query;
	}
	
	
	/**
	 * See IDatabaseDriver::getLastId()
	 */
	
	public function getLastId()
	{
		return mysqli_insert_id($this->handle);
	}
	
	
	/**
	 * See IDatabaseDriver::getNbFields()
	 */
	
	public function getNbFields(&$result)
	{
		return mysqli_num_fields($result);
	}
	
	
	/**
	 * See IDatabaseDriver::getNbAffectedRows()
	 */
	
	public function getNbAffectedRows()
	{
		return mysqli_affected_rows($this->handle);
	}
	
	
	/**
	 * See IDatabaseDriver::getNbRows()
	 */
	
	public function getNbRows(&$result)
	{
		return mysqli_num_rows($result);
	}
	
	
	/**
	 * See IDatabaseDriver::isConnected()
	 */
	
	public function isConnected()
	{
		return (is_null($this->handle) == false);
	}
	

	/**
	 * See IDatabaseDriver::isTableCreated()
	 */
	
	public function isTableCreated($databaseName, $tableName)
	{
		// Get the list of tables (not views)
		
		$query = 'SHOW TABLES FROM `' . $databaseName . '` LIKE \'' . $tableName . '\'';
		$result = $this->executeQuery($query);
		

		if ($this->getNbRows($result) == 1)
		{
			return true;
		}
		
		
		return false;
	}


	/**
	 * See IDatabaseDriver::listTables()
	 */
	
	public function listTables()
	{
		// Get the list of tables (not views)
		
		$query = 'SHOW FULL TABLES WHERE Table_Type = \'BASE TABLE\'';
		$result = $this->executeQuery($query);
		
		$tableNames = array();
		
		while ($row = $this->fetchRow($result))
		{
			$tableNames[] = $row[0];
		}
		
		
		return $tableNames;
	}
	
	
	/**
	 * See IDatabaseDriver::listTablesAndViews()
	 */
	
	public function listTablesAndViews()
	{
		// Get the list of tables and views
		
		$query = 'SHOW TABLES';
		$result = $this->executeQuery($query);
		
		$tableNames = array();
		
		while ($row = $this->fetchRow($result))
		{
			$tableNames[] = $row[0];
		}
		
		
		return $tableNames;
	}
	
	
	/**
	 * See IDatabaseDriver::secureString()
	 */
	
	public function secureString($string)
	{
		// Do we need to connect?
		
		if ($this->isConnected() == false)
		{
			$this->connect();
		}
		
		
		return mysqli_real_escape_string($this->handle, strval($string));
	}


	/**
	 *
	 */

	public function selectDatabase($name)
	{
		if (mysqli_select_db($this->handle, $name) == false)
		{
			// Throw an exception if select of database failed
			
			e
			(
				EXCEPTION_DATABASE_SELECT_FAILED,
				array
				(
					'error' => mysqli_error($this->handle),
					'name' => $name
				)
			);
		}
	}


	/*
	 *
	 */

	public function startTransaction()
	{
		if (pref('[Loop]/database/activeTransaction') == true)
		{
			// Disable autocommit
			
			$query = 'SET AUTOCOMMIT=0';
			$result = $this->executeQuery($query);


			// Start transaction
			
			$query = 'START TRANSACTION';
			$result = $this->executeQuery($query);		
		}
	}
}

?>
